package batchsvg;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.renderable.ParameterBlock;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.List;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.SVGAnimationEngine;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.dom.svg.SVGOMSVGElement;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.XMLResourceDescriptor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.svg.SVGDocument;

public class Main {
	
	public static final String ATTRIBUTE_NAME 		= "name";
    public static final String ATTRIBUTE_VALUE_REFERENCE = "value-reference";
	public static final String ATTRIBUTE_NAME_FILE	= "name-file";
	public static final String ATTRIBUTE_SVG_FILE	= "svg-file";
	public static final String ATTRIBUTE_XML_FILE	= "xml-file";
	public static final String ATTRIBUTE_ADD_IMAGE_PROPS = "add-image-props";
    public static final String ATTRIBUTE_INCLUDE_FILE = "include-file";

	public static final String PROPERTY_OUTPUT_TYPE = "outputType";
	public static final String PROPERTY_ANIMATION_DURATION = "animationDuration";
	public static final String PROPERTY_OUTPUT_SCALE = "outputScale";
    public static final String PROPERTY_ALPHA_MASK_COLOR = "alphaMaskColor";
    public static final String PROPERTY_BUFFERED_IMAGE_TYPE = "bufferedImageType";

    public static final void main(String[] args)
		throws Exception {
		if( args.length != 1 && args.length != 2 ) {
			printUsage();
			System.exit(1);
		}
		String configString = args[0];
		File configFile = new File(configString);
		if( !configFile.exists() ) {
			System.out.println("no such config file "+configFile.getAbsolutePath());
			System.exit(2);
		}
				
		String outputDirString;
		if( args.length > 1 ) {
			outputDirString = args[1];
		} else {
			outputDirString = System.getProperty("user.dir");
		}
		File outputDir;
		outputDir = new File(outputDirString);
		if( !outputDir.exists() ) {
			outputDir.mkdirs();
		}

		DocumentBuilderFactory documentBuilderFactory;
		documentBuilderFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder documentBuilder;
		documentBuilder = documentBuilderFactory.newDocumentBuilder();
		Document document = documentBuilder.parse(configFile);
		Element e = document.getDocumentElement();
		
		File directory = configFile.getParentFile();
		process(e, directory, outputDir, documentBuilder);
	}

    public static final File toFile( File directory, String path ) {
        File file;
        if( path != null ) {
            if( path.startsWith("/") | path.startsWith("\\") ) {
                file = new File(path);
            } else {
                file = new File(directory, path);
            }
        } else {
            file = null;
        }
        return file;
    }
	
	public static final void process(Element e, File directory, File outputDir, DocumentBuilder documentBuilder)
		throws Exception {
		String namePath = e.getAttribute(ATTRIBUTE_NAME_FILE);
		File nameFile = toFile( directory, namePath );
        if( nameFile == null ) {
            throw new Exception("missing attribute "+ATTRIBUTE_NAME_FILE);
        }

		String xslPath = e.getAttribute(ATTRIBUTE_SVG_FILE);
		File xslFile = toFile( directory, xslPath );

		String xmlPath = e.getAttribute(ATTRIBUTE_XML_FILE);
		Document xmlDocument;
		if( xmlPath != null && xmlPath.length() > 0 ) {
			File xmlFile = toFile(directory, xmlPath);
			if(!xmlFile.exists()) {
				throw new FileNotFoundException(xmlFile.getAbsolutePath());
			}
			xmlDocument = documentBuilder.parse(xmlFile);
		} else {
			xmlDocument = documentBuilder.newDocument();
		}
		
		if( xslFile != null && !xslFile.exists() ) {
			throw new FileNotFoundException(xslFile.getAbsolutePath());
		}
		if( !nameFile.exists() ) {
			throw new FileNotFoundException(nameFile.getAbsolutePath());
		}
		
		TransformerFactory transformerFactory;
		transformerFactory = TransformerFactory.newInstance();
		
		StreamSource nameSource = new StreamSource(nameFile);
		Transformer nameTransformer = transformerFactory.newTransformer(nameSource);
		Transformer xslTransformer;
        if( xslFile != null && xslFile.isFile() ) {
		    StreamSource xslSource = new StreamSource(xslFile);
            try {
		        xslTransformer = transformerFactory.newTransformer(xslSource);
            } catch( Exception ex ) {
                throw new Exception("unable to read "+xslFile.getAbsolutePath(), ex);
            }
        } else {
            xslTransformer = null;
        }
		
		NodeList valueSetNodes = e.getChildNodes();
		Map<String,List<BatchParameterValue>> values = new LinkedHashMap<String, List<BatchParameterValue>>(valueSetNodes.getLength());
		for(int i=0; i<valueSetNodes.getLength(); i++) {
			Node node = valueSetNodes.item(i);
			if( node instanceof Element ) {
				Element valueSetElement = (Element)node;
				String valueSetName = valueSetElement.getAttribute(ATTRIBUTE_NAME);
				NodeList valueNodes = valueSetElement.getChildNodes();
				ArrayList<BatchParameterValue> batchParameterValues;
				batchParameterValues = new ArrayList<BatchParameterValue>(valueNodes.getLength());
				for(int j=0; j<valueNodes.getLength(); j++) {
					Node valueNode = valueNodes.item(j);
					if( valueNode instanceof Element ) {
						Element valueElement = (Element)valueNode;
						String value = "";
						NodeList textNodes = valueElement.getChildNodes();
						for( int l=0; l<textNodes.getLength(); l++ ) {
							Node textNode = textNodes.item(l);
							if( textNode instanceof Text ) {
								value += textNode.getTextContent().trim();
							}
						}
						String name = valueElement.getAttribute(ATTRIBUTE_NAME);
						if( name == null || name.trim().length() == 0 ) {
							name = value;
						}
                        if( value == null || value.trim().length() == 0 ) {
                            value = name;
                        }
                        String valueReference = valueElement.getAttribute(ATTRIBUTE_VALUE_REFERENCE);
                        TextualValue textualValue = new TextualValue(value, valueReference);
						BatchParameterValue batchParameterValue;
						batchParameterValue = new BatchParameterValue(name, textualValue);
                        cbeckProperties(directory, valueElement, batchParameterValue, name, textualValue, false);
						
						NodeList propertyNodes = valueNode.getChildNodes();
						for( int k=0; k<propertyNodes.getLength(); k++ ) {
							Node propertyNode = propertyNodes.item(k);
							if( propertyNode instanceof Element ) {
								String propertyName = ((Element) propertyNode).getAttribute(ATTRIBUTE_NAME);
                                String propertyValueReference = ((Element)propertyNode).getAttribute(ATTRIBUTE_VALUE_REFERENCE);
								String propertyValue = propertyNode.getTextContent();
                                TextualValue propertyTextualValue = new TextualValue(propertyValue, propertyValueReference);
								batchParameterValue.getProperties().put(propertyName, propertyTextualValue);
                                cbeckProperties(directory, (Element) propertyNode, batchParameterValue, propertyName, propertyTextualValue, true);
							}
						}
						
						batchParameterValues.add(batchParameterValue);
					}
				}
				values.put(valueSetName, batchParameterValues);
			}
		}
		
		process(documentBuilder,
                transformerFactory,
                directory,
                nameTransformer,
                xslTransformer,
                xmlDocument,
                values,
                outputDir,
                xslFile.toURI().toString()
        );
	}

    public static final BufferedImage getImageForPath(File directory, String value) throws Exception {
        File imageFile;
        if( value.startsWith("/") ) {
            imageFile = new File(value);
        } else {
            imageFile = new File(directory, value);
        }
        try {
            BufferedImage image = ImageIO.read(imageFile);
            return image;
        } catch( Exception ex ) {
            throw new Exception("unable to read image at "+imageFile.getPath(), ex);
        }

    }

    public static final void cbeckProperties(File directory, Element element, BatchParameterValue batchParameterValue, String name, TextualValue textualValue, boolean property)
        throws Exception {
        String imageProps = element.getAttribute(ATTRIBUTE_ADD_IMAGE_PROPS);

        if( "true".equals(imageProps) ) {
            if( textualValue.getReferencedValueKey() != null ) {
                // lazy load this
                batchParameterValue.getProperties().put(name+"-width", new ImageDimensionValue(textualValue.getReferencedValueKey(), directory, true));
                batchParameterValue.getProperties().put(name+"-height", new ImageDimensionValue(textualValue.getReferencedValueKey(), directory, false));

            } else {
                String value = textualValue.getValue();
                BufferedImage image = getImageForPath(directory, value);
                int width = image.getWidth();
                int height = image.getHeight();
                batchParameterValue.getProperties().put(name+"-width", new TextualValue(Integer.toString(width)));
                batchParameterValue.getProperties().put(name+"-height", new TextualValue(Integer.toString(height)));
            }
        }

        String includeFile = element.getAttribute(ATTRIBUTE_INCLUDE_FILE);
        if( "true".equals(includeFile) ) {
            // load the file as the value, not the given value
            // TODO lazy load
            String value = textualValue.getValue();
            File imageFile;
            if( value.startsWith("/") ) {
                imageFile = new File(value);
            } else {
                imageFile = new File(directory, value);
            }
            FileReader reader = new FileReader( imageFile );
            StringWriter writer = new StringWriter((int)imageFile.length());
            int c;
            while( ( c = reader.read() ) >= 0 ) {
                writer.append((char)c);
            }
            if( !property ) {
                batchParameterValue.setValue(new TextualValue(writer.toString()));
            } else {
                batchParameterValue.getProperties().put(name, new TextualValue(writer.toString()));
            }
        }
    }

	
	public static final void process( DocumentBuilder documentBuilder, TransformerFactory transformerFactory, File directory, Transformer nameTransformer, Transformer defaultXslTransformer, Document defaultXml, Map<String,List<BatchParameterValue>> values, File outputDir, String inputUri )
		throws Exception {
		ArrayList<Map.Entry<String,List<BatchParameterValue>>> orderedValues;
		orderedValues = new ArrayList<Map.Entry<String,List<BatchParameterValue>>>(values.size());
		for( Map.Entry<String, List<BatchParameterValue>> entry : values.entrySet() ) {
			orderedValues.add(entry);
		}
		int[] indices = new int[orderedValues.size()];
		Properties properties = new Properties();
		boolean done = false;
		
		do {
			nameTransformer.clearParameters();
			properties.clear();
			
			boolean increment = true;


			for(int i=0; i<indices.length; i++) {
				int index = indices[i];
				Map.Entry<String, List<BatchParameterValue>> entry;
				entry = orderedValues.get(i);
				String parameterName = entry.getKey();
				List<BatchParameterValue> batchParameterValues = entry.getValue();
				BatchParameterValue batchParameterValue;
				batchParameterValue = batchParameterValues.get(index);
				nameTransformer.setParameter(parameterName, batchParameterValue.getName());
                String value = batchParameterValue.getValue().getValue(properties);
				properties.setProperty(parameterName, value);
				for( Map.Entry<String, TextualValue> propertyEntry : batchParameterValue.getProperties().entrySet()) {
					String propertyName = propertyEntry.getKey();
					String propertyValue = propertyEntry.getValue().getValue(properties);
					nameTransformer.setParameter(propertyName, propertyValue);
					properties.setProperty(propertyName, propertyValue);
				}
				
				if(increment) {
					index++;
					if( index >= batchParameterValues.size() ) {
						index = 0;
					} else {
						increment = false;
					}
					indices[i] = index;
				}
			}
			if(increment) {
				done = true;
			}
            String xslPath = properties.getProperty(ATTRIBUTE_SVG_FILE);

            Transformer xslTransformer;
            if( xslPath == null ) {
                xslTransformer = defaultXslTransformer;
            } else {
                // create the xsl
                File xslFile = toFile(directory, xslPath);
                if( !xslFile.exists() ) {
                    throw new Exception("no such file "+xslFile.getAbsolutePath());
                }
                StreamSource source = new StreamSource(xslFile);
                xslTransformer = transformerFactory.newTransformer(source);

            }
            xslTransformer.clearParameters();

            for( Map.Entry<Object, Object> entry : properties.entrySet() ) {
                xslTransformer.setParameter((String)entry.getKey(), entry.getValue());
            }
            String xmlPath = properties.getProperty(ATTRIBUTE_XML_FILE);
            Document xml;
            if( xmlPath == null ) {
                xml = defaultXml;
            } else {
                File xmlFile = toFile(directory, xmlPath);
                if(!xmlFile.exists()) {
                    throw new FileNotFoundException(xmlFile.getAbsolutePath());
                }
                xml = documentBuilder.parse(xmlFile);
            }

			ByteArrayOutputStream namebos = new ByteArrayOutputStream();
			StreamResult nameResult = new StreamResult(namebos);
			nameTransformer.transform(new DOMSource(), nameResult);
			
			StringBuffer sb = new StringBuffer(new String(namebos.toByteArray()));
			for( int i=sb.length(); i>0; ) {
				i--;
				if( Character.isWhitespace(sb.charAt(i)) ) {
					sb.deleteCharAt(i);
				}
			}
			System.out.println("exporting "+sb.toString());
			
			ByteArrayOutputStream rawbos = new ByteArrayOutputStream();
			StreamResult fileResult = new StreamResult(rawbos);
			xslTransformer.transform(new DOMSource(xml), fileResult);
			
			String outputType = properties.getProperty(PROPERTY_OUTPUT_TYPE);
			byte[] rawdata = rawbos.toByteArray();
			
			//String[] imageFileSuffixes = ImageIO.getWriterFileSuffixes();
			String[] imageFileSuffixes = ImageIO.getWriterFormatNames();
			String formatName = null;
			for( int i=0; i<imageFileSuffixes.length; i++ ) {
				if( imageFileSuffixes[i].equalsIgnoreCase(outputType)) {
					formatName = imageFileSuffixes[i];
				}
			}
			
			if( formatName != null ) {
				String animationDurationString = properties.getProperty(PROPERTY_ANIMATION_DURATION);
				Float animationDuration;
				if( animationDurationString != null && animationDurationString.length() > 0 ) {
					animationDuration = new Float(animationDurationString);
				} else {
					animationDuration = null;
				}
				Double scale;
				String scaleString = properties.getProperty(PROPERTY_OUTPUT_SCALE);
				if( scaleString != null && scaleString.length() > 0 ) {
					scale = new Double(scaleString);
				} else {
					scale = null;
				}
                Color alphaMaskColor;
                String transparencyColorString = properties.getProperty(PROPERTY_ALPHA_MASK_COLOR);
                if( transparencyColorString != null ) {
                    alphaMaskColor = Color.decode(transparencyColorString);
                } else {
                    alphaMaskColor = null;
                }

                int bufferedImageType;
                String bufferedImageTypeString = properties.getProperty(PROPERTY_BUFFERED_IMAGE_TYPE);
                if( bufferedImageTypeString != null ) {
                    Field field;
                    field = BufferedImage.class.getDeclaredField(bufferedImageTypeString);
                    bufferedImageType = (Integer)field.get(null);
                } else {
                    bufferedImageType = BufferedImage.TYPE_INT_ARGB;
                }

                Map<Object, Object> hints = new HashMap<Object, Object>();
                addRenderingHint("KEY_ANTIALIASING", RenderingHints.VALUE_ANTIALIAS_ON, properties, hints);
                addRenderingHint("KEY_STROKE_CONTROL", RenderingHints.VALUE_STROKE_NORMALIZE, properties, hints);
                addRenderingHint("KEY_ALPHA_INTERPOLATION", RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY, properties, hints);
                addRenderingHint("KEY_RENDERING", RenderingHints.VALUE_RENDER_QUALITY, properties, hints);
                addRenderingHint("KEY_DITHERING", RenderingHints.VALUE_DITHER_ENABLE, properties, hints);
                addRenderingHint("KEY_COLOR_RENDERING", RenderingHints.VALUE_COLOR_RENDER_QUALITY, properties, hints);


				rawdata = convertToImage( rawdata, formatName, animationDuration, inputUri, scale, alphaMaskColor, bufferedImageType, hints, xslPath);
			}
			
			File outputFile = new File( outputDir, sb.toString() );
            File parentFile = outputFile.getParentFile();
            if( parentFile != null && !parentFile.exists() ) {
                parentFile.mkdirs();
            }
			FileOutputStream fos = new FileOutputStream(outputFile);
			try {
				fos.write(rawdata);
			} finally {
				fos.close();
			}
			
			
		} while(!done);
		
		
	}

    private static void addRenderingHint(String name, Object defaultValue, Properties properties, Map<Object, Object> renderingHints) throws Exception {
        // look up the key
        Field keyField = RenderingHints.class.getDeclaredField(name);
        Object key = keyField.get(null);
        String valueString = properties.getProperty(name);
        Object value;
        if( valueString != null ) {
            Field valueField = RenderingHints.class.getDeclaredField(valueString);
            value = valueField.get(null);
        } else {
            value = defaultValue;
        }
        renderingHints.put(key, value);
    }
	
	private static final byte[] convertToImage(
            byte[] svg,
            String imageType,
            Float animationDuration,String docBaseUri,
            Double scale,
            Color alphaMaskColor,
            int bufferedImageType,
            Map<?,?> renderingHints,
            String sourceFile
    ) throws Exception {
		
		String parser = XMLResourceDescriptor.getXMLParserClassName();
        SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
        SVGDocument svgDocument;
        try {
            svgDocument = f.createSVGDocument(docBaseUri, new ByteArrayInputStream(svg));
        } catch( Exception ex ) {
            throw new Exception("unable to parse svg from \n"+new String(svg), ex);
        }
		
        UserAgentAdapter userAgent = new UserAgentAdapter();
		DocumentLoader loader = new DocumentLoader(userAgent);
		GVTBuilder builder = new GVTBuilder();
        BridgeContext ctx = new BridgeContext(userAgent, loader);
        ctx.setDynamicState(BridgeContext.DYNAMIC);
        GraphicsNode rootGN;
        try {
	        rootGN = builder.build(ctx, svgDocument);
            rootGN.setRenderingHints(renderingHints);

        } catch( Exception ex ) {
        	throw new Exception( "invalid SVG generated\n"+new String( svg ), ex );
        }


        if(animationDuration != null) {
	        SVGAnimationEngine animationEngine = ctx.getAnimationEngine();
	        if( !animationEngine.hasStarted() ) {
	            animationEngine.start(System.currentTimeMillis());          
	        }
	        if( !animationEngine.isPaused() ) {
	            animationEngine.pause();
	        }
	        animationEngine.setCurrentTime(animationDuration);
        }
        
	    SVGOMSVGElement element = (SVGOMSVGElement)svgDocument.getDocumentElement();
	    int width = (int)Math.ceil(element.getWidth().getBaseVal().getValueInSpecifiedUnits());
	    int height = (int)Math.ceil(element.getHeight().getBaseVal().getValueInSpecifiedUnits());
	    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
	    Graphics2D g = GraphicsUtil.createGraphics(image);
        g.addRenderingHints(renderingHints);
	    rootGN.paint(g);
	    g.dispose();

	    if(scale != null) {
	    	int scaledWidth = (int)Math.round(scale.doubleValue()*((double)width));
	    	int scaledHeight = (int)Math.round(scale.doubleValue()*((double)height));
		    Image toWrite;
	    	toWrite = image.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
	    	image = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB);
	    	image.getGraphics().drawImage(toWrite, 0, 0, null);
	    }
        if( alphaMaskColor != null ) {
            // adjust the image so we have a mask
            int rgb = alphaMaskColor.getRGB() & 0xFFFFFF;
            for( int x=0; x<image.getWidth(); x++ ) {
                for( int y=0; y<image.getHeight(); y++ ) {
                    int alpha = (image.getRGB(x, y) & 0xFF000000)>>24;
                    // work out the overlap
                    int maskAlpha = 255 - alpha;
                    int p = rgb | (maskAlpha << 24);
                    image.setRGB(x, y, p);
                }
            }
        }

        image = Ditherer.dither(image, bufferedImageType);

	    ByteArrayOutputStream imageData = new ByteArrayOutputStream();
	    ImageIO.write( image, imageType, imageData);
	    
		return imageData.toByteArray();
	}
	
	private static void printUsage() {
		System.out.println("usage: java "+Main.class.getName()+" <input xml file> [<output dir>]");
		System.out.println("supported image formats are");
		
		String[] imageFileSuffixes = ImageIO.getWriterFormatNames();
		for( int i=0; i<imageFileSuffixes.length; i++ ) {
			System.out.println(imageFileSuffixes[i]);
		}
	}
}
